package org.upfrost.mapping;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.upfrost.Mapper;
import org.upfrost.idiom.SQLIdiom;
import org.upfrost.util.ArrayUtils;


/**
 * Reference the mapping between a java object and an sql table
 * 
 * @author Rodrigo Reyes
 *
 * @param <T> The class of the object being proxied
 */
public class ClassProxy<T> 
{
	private Class<T> m_clazz;
	private HashMap<String,Mapping<?>> m_sqlMapping = new HashMap<String, Mapping<?>>();
	private HashMap<String,Mapping<?>> m_propertyMapping = new HashMap<String, Mapping<?>>();
	private String m_tableName;
	private Mapping<?>[] m_Ids = null;
	private boolean m_IdGenerated = false;
	private Mapper m_mapper;
	
	// caches
	private List<String> m_columnsCache = null;
	private List<String> m_columnsCacheNoKey = null;

	private String m_queryInsertCache = null;
	private String m_queryInsertCacheNoKey = null;
	private String m_queryUpdateCache = null;
	private String m_queryDeleteCache = null;
	
	public ClassProxy(Class clazz, Mapper mapper)
	{
		m_mapper = mapper;
		m_clazz = clazz;
	}
	
	public ClassProxy addMapping(Mapping map)
	{
		m_sqlMapping.put(map.getSqlName(), map);
		m_propertyMapping.put(map.getName(), map);
		
		m_columnsCache = null;
		m_columnsCacheNoKey = null;
		m_queryInsertCache = null;
		m_queryInsertCacheNoKey = null;
		m_queryUpdateCache = null;
		m_queryDeleteCache = null;
		
		return this;
	}

	public void removeMapping(Mapping mapping) 
	{
		m_sqlMapping.remove(mapping.getSqlName());
		m_propertyMapping.remove(mapping.getName());
		
		m_columnsCache = null;
		m_columnsCacheNoKey = null;
		m_queryInsertCache = null;
		m_queryInsertCacheNoKey = null;
		m_queryUpdateCache = null;
		m_queryDeleteCache = null;
	}
	
	public boolean hasProperty(String name)
	{
		return m_propertyMapping.containsKey(name);
	}
	
	public Mapping getPropertyMapping(String propertyName)
	{
		return m_propertyMapping.get(propertyName);
	}
	
	public T get(Mapper mapper, ResultSet row, MetaMapManager mapmanager)
	{
		T instance;
		try {
			instance = (T) m_clazz.newInstance();
		} catch (Exception e) {
			throw new RuntimeException("Error instanciating " + m_clazz);
		}
	
		for (Mapping mapping: m_sqlMapping.values())
		{
			Object obj;
			try {
				obj = retrieveValue(mapper, mapping, row, mapmanager);
				if (obj != null)
				{
					mapping.set(instance, obj);
				}
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
		
		return instance;
	}

	public String getTableName() 
	{
		if (m_tableName == null)
			return m_clazz.getSimpleName();
		
		return m_tableName;
	}

	public void setTableName(String tableName) 
	{
		m_tableName = tableName;
	}

	public Class getMappedClass() {
		return m_clazz;
	}

	public void setMappedClass(Class mClazz) {
		m_clazz = mClazz;
	}
	
	/**
	 * 
	 * Retrieves the value from a result set and a mapping. An optional mapmanager object is used, if available and if possible.
	 * 
	 * @param mapper
	 * @param mapping
	 * @param set
	 * @param mapmanager
	 * @return
	 * @throws SQLException
	 */
	public Object retrieveValue(Mapper mapper, Mapping mapping, ResultSet set, MetaMapManager mapmanager) throws SQLException
	{
		Integer colNumber = null;
		if (mapmanager != null)
		{
			colNumber = mapmanager.getColumn(m_clazz, mapping.getSqlName());
		}
		
		//
		// If we find the exact column with the JoinMapManager, we use it, otherwise we just use the column name.
		Object val = null;
		if (colNumber != null)
			val = set.getObject(colNumber.intValue());
		else
			val = set.getObject(mapping.getSqlName());

		//
		// Convert if needed
//		if (val != null && mapping.getType().isAssignableFrom(val.getClass()) == false)
//		{
//			// need some conversion
//			for (Converter conv: mapper.getConverters())
//			{
//				if (conv.handles(val.getClass(), mapping.getType()))
//				{
//					return conv.convert(val, mapping.getType());
//				}
//			}
//		}
		return val;
	}

	public Mapping[] getIds() {
		return m_Ids;
	}

	public void setIds(Mapping[] ids) {
		m_Ids = ids;
	}

	@Override
	public String toString() {
		return "Proxy(" + m_clazz.getName()+"::"+m_tableName+"::"+m_propertyMapping.values().toString()+")";
	}

	public Set<String> getSqlColumns()
	{
		return m_sqlMapping.keySet();
	}
	
	public Mapping getMappingFromSqlColumn(String column)
	{
		return m_sqlMapping.get(column);
	}

	public List<Mapping<?>> getMappingListCopy()
	{
		LinkedList<Mapping<?>> result = new LinkedList<Mapping<?>>();
		result.addAll(m_propertyMapping.values());
		return result;
	}
	
	public boolean isIdGenerated() {
		return m_IdGenerated;
	}
	
	public void setIdGenerated(boolean idGenerated) {
		m_IdGenerated = idGenerated;
	}
	
//	public String createDelete()
//	{
//		if (m_queryDeleteCache != null)
//			return m_queryDeleteCache;
//		
//		// return m_queryDeleteCache = "delete from " + this.getTableName()+ " where " + this.getId().getSqlName()+"=?";
//		
//		return m_queryDeleteCache = "delete from " + this.getTableName()+ " where " + this.createWherePartOfQueryForKeys();
//	}

	/**
	 * Creates a String that contains a valid sql WHERE part for the keys
	 * ie. COL=? [AND COLx=?]*
	 * @return
	 */
//	public String createWherePartOfQueryForKeys()
//	{
//		SQLIdiom idiom = m_mapper.getIdiom();
//
//		StringBuffer result = new StringBuffer();
//		int index = 0;
//		for (Mapping m: this.getIds())
//		{
//			if (index>0)
//				result.append(" AND ");
//			result.append(idiom.normalizeColumnIdentifier(m.getSqlName())+"=?");
//			index++;
//		}
//		return result.toString();
//	}
	
//	public String createUpdate()
//	{
//		if (m_queryUpdateCache != null)
//			return m_queryUpdateCache;
//
//		SQLIdiom idiom = m_mapper.getIdiom();
//
//		List<String> columns = this.getColumns(true); // ommit the keys
//		
//	   	StringBuffer queryStr = new StringBuffer();
//    	queryStr.append("update " + this.getTableName() + " set ");
//    	int index = 0;
//    	for (String str: columns)
//    	{
//    		if (index++>0)
//    			queryStr.append(",");
//    		queryStr.append(idiom.normalizeColumnIdentifier(str)+"=?");
//    	}
////    	queryStr.append(" WHERE " + this.getId().getSqlName()+"=?");
//    	queryStr.append(" WHERE " + this.createWherePartOfQueryForKeys());
//    	
//    	String result = queryStr.toString();
//    	m_queryUpdateCache = result;
//    	return result;
//	}
	
//	public String createUpdate(List<String> sqlColumns, List<String> sqlKeys)
//	{
//		SQLIdiom idiom = m_mapper.getIdiom();
//
//	   	StringBuffer queryStr = new StringBuffer();
//    	queryStr.append("update " + this.getTableName() + " set ");
//    	int index = 0;
//    	for (String str: sqlColumns)
//    	{
//    		if (index++>0)
//    			queryStr.append(",");
//    		queryStr.append(idiom.normalizeColumnIdentifier(str)+"=?");
//    	}
//    	queryStr.append(" WHERE ");
//    	index = 0;
//    	for (String key: sqlKeys)
//    	{
//    		if (index++>0)
//    			queryStr.append(" AND ");
//    		queryStr.append(key+"=?");
//    	}
// 
//    	String result = queryStr.toString();
//    	return result;
//	}
	
//	public String createInsert(boolean ommitKey)
//	{
//		if (ommitKey && m_queryInsertCacheNoKey != null)
//			return m_queryInsertCacheNoKey;
//		else if (ommitKey==false && m_queryInsertCache != null)
//			return m_queryInsertCache;
//		
//		SQLIdiom idiom = m_mapper.getIdiom();
//		
//		List<String> columns = getColumns(ommitKey);
//		
//    	StringBuffer queryStr = new StringBuffer();
//    	queryStr.append("insert into " + this.getTableName() + " (");
//
//    	int index = 0;
//    	for (String col : columns)
//    	{
//    		if (index++>0)
//    			queryStr.append(",");
//    		queryStr.append(idiom.normalizeColumnIdentifier(col));
//    	}
//    	queryStr.append(") VALUES (");
//    	for (int i=0; i<columns.size(); i++)
//    	{
//    		if (i>0)
//    			queryStr.append(",");
//    		queryStr.append("?");
//    	}
//    	queryStr.append(")");
//
//    	String result = queryStr.toString();
//
//    	if (ommitKey)
//    		m_queryInsertCacheNoKey = result;
//    	else
//    		m_queryInsertCache = result;
//    	
//    	return result;
//	}

	public List<String> getColumnsWithoutTheKeys()
	{
		return getColumns(true);
	}
	
	/**
	 * Return the list of SQL Columns mapped by this class
	 * @param ommitKey
	 * @return
	 */
	public List<String> getColumns(boolean ommitKey)
	{
		if (ommitKey && m_columnsCacheNoKey != null)
			return m_columnsCacheNoKey;
		else if (ommitKey==false && m_columnsCache != null)
			return m_columnsCache;
		
		LinkedList<String> newcolumns = new LinkedList<String>();
		newcolumns.addAll(getSqlColumns());
		if (ommitKey)
		{
//			newcolumns.remove(this.getId().getSqlName());
			for (Mapping m: this.getIds())
				newcolumns.remove(m.getSqlName());
			m_columnsCacheNoKey = newcolumns;
		}
		else
		{
			m_columnsCache = newcolumns;
		}
		return newcolumns;
	}

	/**
	 * Return the list of SQL Columns mapped by this class NOT including the mapping which property are contained in the list provided
	 * @param properties the properties to ommit in the list 
	 * @return
	 */
	public List<String> getColumnsWithoutProperties(String... properties)
	{
		LinkedList<String> newcolumns = new LinkedList<String>();

		for (Mapping map: m_sqlMapping.values())
		{
			if (ArrayUtils.contains(map.getName(), properties) == false)
			{
				newcolumns.add(map.getSqlName());
			}
		}

		return newcolumns;
	}

	
	/**
	 * Return true if the key of this object instance is considered as "to be generated". This is the 
	 * case when the id has the @GeneratedValue annotation AND if the mapper.isIdSet() method return null
	 * when tested against this id value; This method is typically used for insert queries, to get the 
	 * information on whether the key must be omitted from the query (because it's automatically generated
	 * by the database) or not (when the key is explicitly defined in the query).
	 * 
	 * @param mapper
	 * @param instance
	 * @return
	 */
	public boolean isKeyGenerated(Mapper mapper, Object instance)
	{
		if (this.getIds()!= null && this.getIds().length == 1)
		{		
	    	Object keyValue = this.getIds()[0].get(instance);
	
			if (this.isIdGenerated() && (mapper.isIdSet(keyValue)==false))
	    	{
				return true;
	    	}
		}
		return false;
	}

	public boolean hasKey()
	{
		return m_Ids.length>0;
	}
	
	public boolean hasOneKey()
	{
		return m_Ids.length == 1;
	}
}
